#!/usr/bin/perl
use strict;
use FindBin;

use JSON;
use REST::Client;
use Getopt::Long;
use JSON qw(to_json from_json);
use POSIX qw(strftime);

use AutoExecUtils;

sub usage {
    my $pname = $FindBin::Script;

    print("Usage: $pname --host K8sHost --apiport APIServerPort --mgmtport ManagermentPort --token Token\n");
    print("       --image DockerImage --name AppName  --namespace K8sNameSpace --deployment K8sDeployment\n");
    print("\n");
    print("       --host:         k8s host, example:192.168.99.100\n");
    print("       --apiport:      k8s api port, example:8843\n");
    print("       --mgmtport:     k8s web management port , example:30001\n");
    print("       --token:        k8s authenticate token\n");
    print("       --image:        docker image, example:nginx:1.7.6\n");
    print("       --name:         app name, example:nginx\n");
    print("       --namespace:    k8s namespace, example:default\n");
    print("       --deployment:   k8s deployment: example:nginx-deployment\n");
    print("       --acton:        k8s deploy action patch|del\n");

    exit(1);
}

sub doGet {
    my ( $url, $headers ) = @_;

    my $client = REST::Client->new();
    $client->addHeader( 'Content-Type', 'application/json' );
    if ( defined($headers) ) {
        foreach my $keyName ( keys(%$headers) ) {
            $client->addHeader( $keyName, $headers->{$keyName} );
        }
    }

    $client->getUseragent()->ssl_opts( verify_hostname => 0 );
    $client->getUseragent()->ssl_opts( SSL_verify_mode => 0 );
    $client->GET($url);
    my $data;
    if ( $client->responseCode() ne 200 ) {
        my $errMsg = $client->responseContent();
        print("ERROR: Get restful api:$url failed, cause by:$errMsg\n");
        exit(1);
    }
    else {
        my $content = $client->responseContent();
        $data = from_json($content);
    }
    return $data;
}

sub doPost {
    my ( $url, $param, $headers ) = @_;

    my $client = REST::Client->new();
    $client->addHeader( 'Content-Type', 'application/json' );
    if ( defined($headers) ) {
        foreach my $keyName ( keys(%$headers) ) {
            $client->addHeader( $keyName, $headers->{$keyName} );
        }
    }

    $client->getUseragent()->ssl_opts( verify_hostname => 0 );
    $client->getUseragent()->ssl_opts( SSL_verify_mode => 0 );
    $client->POST( $url, $param );
    my $data;
    if ( $client->responseCode() ne 200 ) {
        my $errMsg = $client->responseContent();
        print("ERROR: Get restful api:$url failed, cause by:$errMsg\n");
        exit(1);
    }
    else {
        my $content = $client->responseContent();
        $data = from_json($content);
    }
    return $data;
}

sub login {
    my ( $baseUrl, $token ) = @_;

    my $csrfUrl = $baseUrl . "/api/v1/csrftoken/login";
    my $csrfRes = doGet($csrfUrl);
    my $csrf    = $csrfRes->{'token'};

    my $v1Url   = $baseUrl . "/api/v1/login";
    my $param   = { 'token' => $token };
    my $data    = to_json($param);
    my $headers = { 'x-csrf-token' => $csrf };

    my $res      = doPost( $v1Url, $data, $headers );
    my $jweToken = $res->{'jweToken'};
    return $jweToken;
}

sub getDeployment {
    my ( $baseUrl, $jwetoken, $namespace, $deploymentName ) = @_;
    my $url     = $baseUrl . "/api/v1/deployment/$namespace/$deploymentName";
    my $headers = { 'jwetoken' => $jwetoken };
    my $res     = doGet( $url, $headers );
    return $res;
}

sub getRplicaset {
    my ( $baseUrl, $jwetoken, $namespace, $deploymentName ) = @_;
    my $newrsUrl = $baseUrl . "/api/v1/deployment/$namespace/$deploymentName/newreplicaset";
    my $headers  = { 'jwetoken' => $jwetoken };
    my $newRes   = doGet( $newrsUrl, $headers );

    #历史副本只取最新1条
    my $oldrsUrl = $baseUrl . "/api/v1/deployment/$namespace/$deploymentName/oldreplicaset?itemsPerPage=1&page=1&sortBy=d,creationTimestamp";
    my $oldRes   = doGet( $oldrsUrl, $headers );

    return ( $newRes, $oldRes );
}

sub getPods {
    my ( $baseUrl, $jwetoken, $namespace, $podName ) = @_;

    #pods数量上限最多100
    my $url     = $baseUrl . "/api/v1/replicaset/$namespace/$podName/pod?itemsPerPage=100&page=1&sortBy=d,creationTimestamp";
    my $headers = { 'jwetoken' => $jwetoken };
    my $res     = doGet( $url, $headers );

    return $res;
}

sub getRollUpdateInfo {
    my ( $baseUrl, $token, $namespace, $deploymentName ) = @_;
    my $data     = {};
    my $jwetoken = login( $baseUrl, $token );
    if ( defined($jwetoken) ) {
        my $deployment = getDeployment( $baseUrl, $jwetoken, $namespace, $deploymentName );
        if ( defined($deployment) ) {
            $data->{'deployment'} = $deployment;
        }

        my ( $newRs, $oldRs ) = getRplicaset( $baseUrl, $jwetoken, $namespace, $deploymentName );

        #最新副本
        if ( defined($newRs) ) {
            $data->{'newreplicaset'} = $newRs;
            my $rsName = $newRs->{'objectMeta'}->{'name'};
            my $pods   = getPods( $baseUrl, $jwetoken, $namespace, $rsName );
            if ( defined($pods) ) {
                $data->{'newreplicaset'}->{'pod'} = $pods;
            }
            else {
                $data->{'newreplicaset'}->{'pod'} = undef;
            }
        }
        else {
            $data->{'newreplicaset'} = undef;
        }

        #上个副本
        if ( defined($oldRs) ) {
            my $prevRs = $oldRs->{'replicaSets'};
            if ( defined($prevRs) && scalar(@$prevRs) > 0 ) {
                my $prevRsIns = @$prevRs[0];
                $data->{'oldreplicaset'} = $prevRsIns;
                my $rsName = $prevRsIns->{'objectMeta'}->{'name'};
                my $pods   = getPods( $baseUrl, $jwetoken, $namespace, $rsName );
                if ( defined($pods) ) {
                    $data->{'oldreplicaset'}->{'pod'} = $pods;
                }
                else {
                    $data->{'oldreplicaset'}->{'pod'} = undef;
                }
            }
            else {
                $data->{'oldreplicaset'} = undef;
            }
        }
        else {
            $data->{'oldreplicaset'} = undef;
        }
    }
    return $data;
}

sub getPrevVerImage {
    my ( $client, $url, $image ) = @_;
    my $preVerImage;
    my $causeBy = '';
    $client->GET($url);
    if ( $client->responseCode() ne 200 ) {
        my $errMsg = $client->responseContent();
        $causeBy = "ERROR: Rollback application $image failed, cause by:$errMsg\n";
    }
    else {
        my $content    = $client->responseContent();
        my $data       = from_json($content);
        my $rsItems    = $data->{'items'};
        my $imgMap     = {};
        my $currentGen = 0;
        foreach my $rs (@$rsItems) {
            my $img;
            my $containers = $rs->{'spec'}->{'template'}->{'spec'}->{'containers'};
            my $gen        = int( $rs->{'metadata'}->{'generation'} );
            foreach my $ct (@$containers) {
                $img = $ct->{'image'};
                if ( $image eq $img ) {
                    $currentGen = $gen;
                    last;
                }
            }
            $imgMap->{$gen} = $img;
        }
        my $prevGen = $currentGen + 1;
        $preVerImage = $imgMap->{$prevGen};
        if ( not defined($preVerImage) or $preVerImage eq '' ) {
            $causeBy = "ERROR: Rollback application $image failed, cause by: can not found history version.\n";
        }
    }
    return ( $preVerImage, $causeBy );
}

sub main {
    my ( $host, $apiPort, $managerPort, $token );
    my ( $image, $name, $namespace, $deployment, $action, $timeout );

    GetOptions(
        'host=s'       => \$host,
        'apiport=s'    => \$apiPort,
        'mgmtport=s'   => \$managerPort,
        'token=s'      => \$token,
        'image=s'      => \$image,
        'name=s'       => \$name,
        'namespace=s'  => \$namespace,
        'deployment=s' => \$deployment,
        'action=s'     => \$action,
        'timeout=i'    => \$timeout
    );

    my $optionError = 0;

    if ( ( not defined($host) or $host eq '' ) and ( not defined($apiPort) or $apiPort eq '' ) ) {
        print("ERROR: Must defind ip and apiPort for k8s api server.\n");
        $optionError = 1;
    }

    if ( not defined($token) or $token eq '' ) {
        print("ERROR: Must defind Authenticate token for k8s api server.\n");
        $optionError = 1;
    }

    if ( not defined($image) or $image eq '' ) {
        print("ERROR: Must defind docker image\n");
        $optionError = 1;
    }

    if ( not defined($name) or $name eq '' ) {
        print("ERROR: Must defind app name\n");
        $optionError = 1;
    }

    if ( not defined($namespace) or $namespace eq '' ) {
        print("ERROR: Must defind namespace\n");
        $optionError = 1;
    }

    if ( not defined($deployment) or $deployment eq '' ) {
        print("ERROR: Must defind deployment name\n");
        $optionError = 1;
    }

    if ( not defined($timeout) ) {
        $timeout = 120;
    }
    my $org_timeout = $timeout;

    if ( $optionError == 1 ) {
        usage();
    }

# my $name = 'nginx';
# my $image = 'nginx:1.7.6';
# my $baseUrl = 'https://192.168.99.100:8443';
# my $namespace = 'default';
# my $deployment = 'nginx-deployment';
# my $token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLXo4ZmYyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIwZWJmMTFkNS00NDI2LTQ3ZjgtODA2OS1kNjNhZWY1NmIzZTkiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06YWRtaW4tdXNlciJ9.qhFQ1y4GsvwhjfSKPrvWx7sHAgrwPitCID9j7otXD6bnxIVcreb_cfPN_9L_S2h8wOp4qsqWWOCXTpK84xlTIIY4Jk2jjBl2keJOW64oI2TN2gxAE368iAC9EnlCF0kyd7_-a-A0zbhRszd0-zD_7Pn3ejzQbqqouUUIzBAcD3Bj4Dmr95tnEBUI4NbtnrZB8AUb9BjlBjr8NqXpXQjNJiUfXSW6TVhxYGSAtSBzacHSWpXAkoiw6lqLaCz4hL-8sXGvLjRsC3klLLt73jATz3eDmdMHmZB7OtAEK2U10VxPTMKvxiIZptGF_8o5proLXpd4ft0ChBgRgPAck5BJNA';
    my $baseUrl = "https://$host:$apiPort";
    my $url     = "$baseUrl/apis/apps/v1/namespaces/$namespace/deployments/$deployment";

    my $client = REST::Client->new();
    $client->addHeader( "Authorization", "Bearer $token" );
    $client->addHeader( 'Content-Type',  'application/strategic-merge-patch+json' );
    $client->getUseragent()->ssl_opts( verify_hostname => 0 );
    $client->getUseragent()->ssl_opts( SSL_verify_mode => 0 );

    if ( $action eq 'rollback' ) {
        my $replicasetsUrl = "$baseUrl/apis/apps/v1/namespaces/$namespace/replicasets?labelSelector=app%3D$name";
        my ( $prevImage, $causeBy ) = getPrevVerImage( $client, $replicasetsUrl, $image );
        if ( not defined($prevImage) or $prevImage eq '' ) {
            print($causeBy);
            exit(1);
        }

        #用上个版本镜像回退
        $image = $prevImage;
    }

    my $reqObj = {
        spec => {
            template => {
                spec => {
                    containers => [
                        {
                            name  => $name,
                            image => $image
                        }
                    ]
                }
            }
        }
    };
    if ( $action eq 'restart' ) {
        $reqObj->{spec}->{metadata} = {
            annotations => {
                'kubectl.kubernetes.io/restartedAt' => strftime( "%Y-%m-%d %H:%M:%S", localtime() )
            }
        };
    }
    my $hasError = 0;
    my $queryData;
    my $req = to_json($reqObj);

    #TODO: 增加其他的action，譬如：del、put等
    if ( $action eq 'patch' or $action eq 'rollback' or $action eq 'restart' ) {
        $client->PATCH( $url, $req );
    }
    else {
        print("ERROR: Action:$action not supported.\n");
        $hasError = $hasError + 1;
        return $hasError;
    }

    if ( $client->responseCode() ne 200 ) {
        my $errMsg = $client->responseContent();
        print("ERROR: Patch application $name(nanmespace:$namespace, deployment:$deployment) with $image failed, cause by:$errMsg\n");
        $hasError = $hasError + 1;
    }
    else {
        my $content = $client->responseContent();
        my $data    = from_json($content);

        my $name  = $data->{'spec'}->{'template'}->{'spec'}->{'containers'}[0]->{'name'};
        my $image = $data->{'spec'}->{'template'}->{'spec'}->{'containers'}[0]->{'image'};

        #TODO：需要获取滚动升级的信息以及日志，并push到页面进行显示，检测全部滚动升级完成
        $baseUrl = "https://$host:$managerPort";
        while (1) {
            $queryData = getRollUpdateInfo( $baseUrl, $token, $namespace, $deployment );
            if ( defined($queryData) ) {

                if ( not defined( $queryData->{'newreplicaset'} ) or not defined( $queryData->{'newreplicaset'}->{'pod'} ) ) {
                    last;
                }

                #print(to_json($queryData));
                my $pod     = $queryData->{'newreplicaset'}->{'pod'};
                my $pods    = $pod->{'pods'};
                my $desired = int( $pod->{'listMeta'}->{'totalItems'} );
                my $pending = 0;
                my $running = 0;
                my $failed  = 0;

                #状态转换，0 正常；1异常 ；2 初始化中
                my $statusMap = {
                    'Running'                               => 0,
                    'CrashLoopBackOff'                      => 1,    #容器退出，kubelet正在将它重启
                    'InvalidImageName'                      => 1,    #无法解析镜像名称
                    'ImageInspectError'                     => 1,    #无法校验镜像
                    'ErrImageNeverPull'                     => 1,    #策略禁止拉取镜像
                    'ImagePullBackOff'                      => 2,    #正在重试拉取
                    'RegistryUnavailable'                   => 1,    #连接不到镜像中心
                    'ErrImagePull'                          => 1,    #拉取镜像出错
                    'CreateContainerConfigError'            => 1,    #不能创建kubelet使用的容器配置
                    'CreateContainerError'                  => 1,    #创建容器失败
                    'm.internalLifecycle.PreStartContainer' => 1,    #执行hook报错
                    'RunContainerError'                     => 1,    #启动容器失败
                    'PostStartHookError'                    => 1,    #执行hook报错
                    'ContainersNotInitialized'              => 2,    #容器没有初始化完毕
                    'ContainersNotReady'                    => 2,    #容器没有准备完毕
                    'ContainerCreating'                     => 2,    #容器创建中
                    'PodInitializing'                       => 2,    #pod 初始化中
                    'ContainersNotReady'                    => 2,    #容器没有准备完毕
                    'DockerDaemonNotReady'                  => 2,    #docker还没有完全启动
                    'NetworkPluginNotReady'                 => 2     #网络插件还没有完全启动
                };

                foreach my $pod (@$pods) {
                    my $podStatus    = $pod->{'status'};
                    my $podStatusVal = $statusMap->{$podStatus};
                    if ( not defined($podStatusVal) ) {
                        $podStatusVal = 2;
                    }
                    if ( $podStatusVal == 0 ) {
                        $running++;
                    }
                    elsif ( $podStatusVal == 1 ) {
                        $failed++;
                    }
                    elsif ( $podStatusVal == 2 ) {
                        $pending++;
                    }
                }

                print("INFO: Desired pod: $desired, Pending pod: $pending, Running pod: $running, Failed pod: $failed .\n");
                if ( $desired == ( $running + $failed ) ) {
                    print("INFO: Desired pod: $desired, Running pod: $running, Failed pod: $failed.\n");
                    if ( $failed > 0 ) {
                        $hasError = 1;
                    }
                    last;
                }
                else {
                    sleep(3);
                    $timeout--;
                }
            }
            else {
                last;
            }

            if ( $timeout <= 0 ) {
                $hasError = 1;
                print("ERROR: Read application $name(nanmespace:$namespace, deployment:$deployment) with $image timeout ,waiting $org_timeout second.\n");
                last;
            }
        }

        if ( $hasError > 0 ) {
            print("ERROR: Patch application $name(nanmespace:$namespace, deployment:$deployment) with $image failed.\n");
        }
        else {
            print("FINE: Patch application $name(nanmespace:$namespace, deployment:$deployment) with $image success.\n");
        }
    }

    my $out = {};
    $out->{k8s_data} = $queryData;
    AutoExecUtils::saveOutput($out);
    AutoExecUtils::saveLiveData($out);

    return $hasError;
}

exit main();
